/*
 * Amadeus Confidential Information:
 * Unauthorized use and disclosure strictly forbidden.
 * @1998-2007 - Amadeus s.a.s - All Rights Reserved.
 */
package com.amadeus.spin.devtool.soap.shooter.filter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.amadeus.spin.devtool.soap.shooter.config.SoapShooterInitializationException;

/**
 * TODO : set the m_dateValue instead of all this and then use simpledateformat
 * and dateOffset to return the correct date string format
 */
public class DateFilter extends FilterImpl {

  public static final String YEAR = "year";

  public static final String MONTH = "month";

  public static final String WEEK = "week";

  public static final String TODAY = "today";

  public static final long DAY_MILLISEC = 24 * 60 * 60 * 1000L;

  private static final String DATE_ETV = "ETV";

  private static final String DATE_STD = "STD";

  private static final String DATE_FORMAT_STR = "yyyyMMddhhmm";

  private static final String DATE_STR_PATTERN = "\\d{" + DATE_FORMAT_STR.length() + "}";

  private static final String TIME_PATTERN_REG = ".*@(\\d{4}).*";

  private static final Pattern TIME_PATTERN = Pattern.compile(TIME_PATTERN_REG);

  private static final String ETVFORMAT = "yyyyMMddHHmm";

  private static final String STD_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";

  private static final String CC_FORMAT = "MM/yy";

  private static Logger log = Logger.getLogger(DateFilter.class);

  private int mDay;

  private int mMonth;

  private int mYear;

  private int mHour;

  private int mMinute;

  private int mCount;

  public DateFilter(String p_filterName, String p_filterValue)
      throws SoapShooterInitializationException {
    super(p_filterName, p_filterValue);
    if (!setDate(p_filterValue)) {
      throw (new SoapShooterInitializationException("Filter (" + p_filterName + ", " + p_filterValue +
          ") badly formed."));
    }
  }

  /**
   * set Date (based on input String). The date string can be one of:
   * <ul>
   * <li>If one value (no pluses) the it should be a ddmmyyyy or keyword "today".
   * <li>If it is a composition based on tokens (pluses), one should be "today" which is the base date to which you can
   * add/ a "month" which pushed to the same day of the following month. You can also add a figure in days or a "week"
   * which
   * </ul>
   *
   * @param dateStr
   * @return true, date ok, false problem with date.
   */
  protected boolean setDate(String dateStr) {
    if (hasToken(dateStr)) {
      // tokens
      return setUpDateModifier(dateStr);
    }
    // no tokens, should be either today or ddmmyyyy
    return setupDateNoModifier(dateStr);
  }

  private boolean hasToken(String dateStr) {
    if (dateStr != null) {
      return (dateStr.contains("+") || dateStr.contains("-"));
    }
    return false;
  }

  /**
   * @param dateStr
   * @return
   */
  private boolean setUpDateModifier(String dateStr) {
    int xdays = 0;

    Calendar time = new GregorianCalendar();
    time.setTimeInMillis(0);

    mCount = 0;

    Date today = null;
    boolean todayFound = false;

    String hourMin = getTimeTag(dateStr);
    if (hourMin != null) {
      // Always 4 digits
      time.add(Calendar.MILLISECOND,
          (Integer.parseInt(hourMin.substring(0, 2)) * 60 * 60 * 1000)
              + (Integer.parseInt(hourMin.substring(2, 4))) * 60 * 1000);
      // Remove the time from the string
      dateStr = StringUtils.remove(dateStr, "@" + hourMin);
    }

    // tokens with delimiter separated by char.: + or -
    StringTokenizer st = new StringTokenizer(dateStr.toLowerCase(), "+-", true);
    String s = null;
    int operationSign = 1;

    while (st.hasMoreTokens()) {
      s = st.nextToken();

      if (s.equals(TODAY)) {
        // Check if the hour/min are already set
        if (hourMin != null) {
          // Time already set: get only the date
          Calendar cToday = Calendar.getInstance();

          cToday.set(Calendar.HOUR_OF_DAY, 0);
          cToday.set(Calendar.MINUTE, 0);
          cToday.set(Calendar.SECOND, 0);
          cToday.set(Calendar.MILLISECOND, 0);

          // Add the value
          time.setTimeInMillis(cToday.getTime().getTime() + time.getTimeInMillis());
          today = new Date();
        }
        else {
          // Get the current date/time
          today = new Date();
          // milliseconds
          time.setTimeInMillis(today.getTime() + time.getTimeInMillis());
        }
        todayFound = true;
        continue;
      }

      // check of plus or minus
      if (s.equals("+")) {
        operationSign = 1;
        continue;
      }
      else if (s.equals("-")) {
        operationSign = -1;
        continue;
      }

      // +/-XXXweek
      if (s.endsWith(WEEK)) {
        int nbWeek = extractNumber(s, WEEK);
        if (nbWeek == -1) {
          return false;
        }

        time.add(Calendar.WEEK_OF_YEAR, operationSign * nbWeek);
        continue;
      }

      // +/-XXXmonth
      if (s.endsWith(MONTH)) {
        int nbMonth = extractNumber(s, MONTH);
        if (nbMonth == -1) {
          return false;
        }
        time.add(Calendar.MONTH, operationSign * nbMonth);
        continue;
      }

      // +/-XXXyear
      if (s.endsWith(YEAR)) {
        int nbYear = extractNumber(s, YEAR);
        if (nbYear == -1) {
          return false;
        }
        time.add(Calendar.YEAR, operationSign * nbYear);
        continue;
      }

      // next_YYYYYYY with YYYYYYY a day of week
      if (s.toLowerCase().startsWith("next")) {
        if (todayFound) {
          nextDayOfTheWeek(s, time.getTimeInMillis());
        }
        time.add(Calendar.DAY_OF_YEAR, mCount);
        continue;
      }

      // +/-XX days
      try {
        xdays = Integer.parseInt(s);
      }
      catch (NumberFormatException e) {
        log.error("'" + s + "' is not supported by the DateFilter");
        return false;
      }
      time.add(Calendar.DAY_OF_YEAR, operationSign * xdays);
    }

    if (todayFound) {
      today.setTime(time.getTimeInMillis());
      Calendar c = new GregorianCalendar();
      c.setTime(today);
      setDateAndTimeMembersFromCalendar(c);
    }
    else {
      log.error("'Today' must be defined in the DateFilter");
      return false;
    }
    return true;
  }


  /**
   * Process the time if it exists : ends with '@HHMM'
   *
   * @param dateStr
   * @return
   */
  private String getTimeTag(String dateStr) {
    String hourMin = null;
    Matcher m = TIME_PATTERN.matcher(dateStr);
    if (m.matches()) {
      hourMin = m.group(1);
    }
    return hourMin;
  }

  private boolean setupDateNoModifier(String dateStr) {
    Date date = null;
    Calendar cal = new GregorianCalendar();
    if (dateStr.equalsIgnoreCase(TODAY)) {
      date = new Date();
    }
    else if (Pattern.matches(DATE_STR_PATTERN, dateStr)) {
      SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_STR);
      try {
        date = dateFormat.parse(dateStr);
      }
      catch (ParseException parseExc) {
        return false;
      }
    }
    else {
      return false;
    }
    cal.setTime(date);

    setDateAndTimeMembersFromCalendar(cal);
    return true;
  }

  private void setDateAndTimeMembersFromCalendar(Calendar initializedCal) {
    mDay = initializedCal.get(java.util.Calendar.DAY_OF_MONTH);
    mMonth = initializedCal.get(java.util.Calendar.MONTH) + 1;
    mYear = initializedCal.get(java.util.Calendar.YEAR);
    mHour = initializedCal.get(java.util.Calendar.HOUR_OF_DAY);
    mMinute = initializedCal.get(java.util.Calendar.MINUTE);
  }

  private int extractNumber(String dateStr, String type) {
    try {
      if (dateStr.equals(type)) {
        return 1;
      }
      String numberStr = dateStr.substring(0, dateStr.indexOf(type));
      return Integer.parseInt(numberStr);
    }
    catch (NumberFormatException e) {
      // in case of any problem, return -1
      return -1;
    }
  }

  /**
   * Define an enum to simplify the treatment of next_dates
   *
   */
  private enum NEXT_DAYS {
    next_sunday(1), next_monday(2), next_tuesday(3), next_wednesday(4), next_thursday(5), next_friday(6),
    next_saturday(7);
    private final int idx;

    NEXT_DAYS(int index) {
      this.idx = index;
    }

    public int getIdx() {
      return idx;
    }
  }

  /**
   * Define the number of days (m_count) to add to original date xxx (today or
   * ddMMMyyyy) from the nextDayOfTheWeek offset tag (xxx+NEXT_yyy). yyy can be
   * any of the week day (MONDAY, TUESDAY, etc...).
   *
   * @param s
   *          - StringTokenizer token (ex: "NEXT_SUNDAY")
   * @param today
   *          - today's Date object
   */
  public void nextDayOfTheWeek(String s, long time) {
    int nextDayOfWeek = 0;

    for (NEXT_DAYS nextDayEnum : NEXT_DAYS.values()) {
      if (nextDayEnum.name().equalsIgnoreCase(s)) {
        nextDayOfWeek = nextDayEnum.getIdx();
      }
    }
    if (nextDayOfWeek == 0) {
      // if wrong offset tag formatted, log an error and keep the original date
      log.error("Problem with offset formatting: " + s);
    }

    Date date = new Date();
    date.setTime(time);
    Calendar cal = new GregorianCalendar();
    cal.setTime(date);
    int todayDayOfWeek = cal.get(java.util.Calendar.DAY_OF_WEEK);

    if (nextDayOfWeek > 0) {
      if (nextDayOfWeek <= todayDayOfWeek) {
        for (int j = todayDayOfWeek; j < Calendar.SATURDAY; j++) {
          mCount++;
        }
        mCount += nextDayOfWeek;
      }
      else {
        mCount = nextDayOfWeek - todayDayOfWeek;
      }
    }
  }

  /**
   * Method declaration
   *
   * @return
   */
  public int getDay() {
    return mDay;
  }

  /**
   * Method declaration
   *
   * @return
   */
  public int getMonth() {
    return mMonth;
  }

  /**
   * Method declaration
   *
   * @return
   */
  public int getYear() {
    return mYear;
  }

  /**
   * Method declaration
   *
   * @return
   */
  public int getMinute() {
    return mMinute;
  }

  /**
   * Method declaration
   *
   * @return
   */
  public int getHour() {
    return mHour;
  }

  /**
   * build a string for host script request based on type. A date for an input
   * script can be of different forms.
   *
   * @param day
   * @param month
   * @param year
   * @param dateType
   *          cryptic( ddMMM e.g: 12MAY) , old (ddMMMyyyy e.g.: 12MAY2002), new
   *          (ddmmyy e.g.: 120502)
   * @return
   */
  public static String buildDate(int day, int month, int year, int hour, int minute, String dateType) {
    String date = null;

    if (dateType.equalsIgnoreCase(DATE_STD)) {
      date = buildSTDDate(day, month, year, hour, minute);
    }
    else if (dateType.equalsIgnoreCase(DATE_ETV)) {
      date = buildETVDate(day, month, year, hour, minute);
    }
    else if (dateType.equalsIgnoreCase("CC")) {
      date = buildCCDate(month, year);
    }
    else {
      // If the parameter looks like a date format, it is used, otherwise the default format is used
      try {
        date = buildCustomFormatDate(day, month, year, hour, minute, dateType);
      }
      catch (Exception e) {
        log.warn("\"" + dateType + "\" is not a valid Date Format. Using STD format instead.");
        date = buildSTDDate(day, month, year, hour, minute);
      }
    }
    return date;
  }

  String buildDate() {
    return buildDate(DATE_ETV);
  }

  String buildDate(String fieldType) {
    return buildDate(getDay(), getMonth(), getYear(), getHour(), getMinute(), fieldType);
  }

  /**
   * Build Date of format YYYY-mm-ddThh:mm:ss, 2002-09-25T18:00:00
   *
   * @param day
   * @param month
   * @param year
   * @param hour
   * @param minutes
   * @return formated date
   */
  public static String buildSTDDate(int day, int month, int year, int hour, int minute) {
    Calendar today = createCalendar(day, month, year, hour, minute);
    return new SimpleDateFormat(STD_FORMAT).format(today.getTime());
  }

  /**
   * Format YYYYMMddhhmmss 20020925180000
   *
   * @param day
   * @param month
   * @param year
   * @param hour
   * @param minute
   * @return
   */
  public static String buildETVDate(int day, int month, int year, int hour, int minute) {
    Calendar today = createCalendar(day, month, year, hour, minute);
    return new SimpleDateFormat(ETVFORMAT).format(today.getTime());
  }

  /**
   * Creates a calendar from input date data
   *
   * @param day
   * @param month
   * @param year
   * @param hour
   * @param minute
   * @return
   */
  private static Calendar createCalendar(int day, int month, int year, int hour, int minute) {
    Calendar today = Calendar.getInstance();
    today.set(Calendar.YEAR, year);
    today.set(Calendar.MONTH, month - 1);
    today.set(Calendar.DAY_OF_MONTH, day);
    today.set(Calendar.HOUR_OF_DAY, hour);
    today.set(Calendar.MINUTE, minute);
    today.set(Calendar.SECOND, 0);
    return today;
  }

  /**
   * Builds a date with the MM/YY format
   *
   * @param month
   * @param year
   * @return
   */
  public static String buildCCDate(int month, int year) {
    Calendar today = Calendar.getInstance();
    today.set(Calendar.SECOND, 0);
    today.set(Calendar.YEAR, year);
    today.set(Calendar.MONTH, month - 1);

    return new SimpleDateFormat(CC_FORMAT).format(today.getTime());
  }

  /**
   * Builds the date string from a custom format, passed as a parameter
   *
   * @param day
   * @param month
   * @param year
   * @param hour
   * @param minute
   * @param format
   *          , must be supported by the java SimpleDateFormat
   * @return
   */
  public static String buildCustomFormatDate(int day, int month, int year, int hour, int minute, String format) {
    SimpleDateFormat dateFormat = new SimpleDateFormat(format);
    Calendar cal = new GregorianCalendar(year, month - 1, day, hour, minute);
    String date = dateFormat.format(cal.getTime());
    return date;
  }

  @Override
  public String getFormattedValue(String format) {
    return buildDate(format);
  }

  @Override
  public String getDefaultValue() {
    return buildDate();
  }

  @Override
  public String getTypeString() {
    return DATE_TYPE;
  }
}
